﻿#region License
/*
The MIT License

Copyright (c) 2008 Sky Morey

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#endregion
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Principal;
using System.Text;
namespace System.DirectoryServices
{
    public static class LdapExtensions
    {
        private static char[] _splitChars = new[] { '/' };

        public class QueryInfo
        {
            public QueryInfo()
            {
                BatchSize = 1000;
                QueryFilter = "{0}";
            }
            public int BatchSize { get; set; }
            public string QueryFilter { get; set; }
            public Action<DirectorySearcher> Limiter { get; set; }
        }

        public static string GetDN(this DirectoryEntry source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var split = source.Path.Split(_splitChars);
            return split[split.Length - 1];
        }
        public static string GetDN(this SearchResult source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var split = source.Path.Split(_splitChars);
            return split[split.Length - 1];
        }

        public static string GetName(this DirectoryEntry source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var split = source.Path.Split(_splitChars);
            return Ldap.GetNameFromDN(split[split.Length - 1]);
        }
        public static string GetName(this SearchResult source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var split = source.Path.Split(_splitChars);
            return Ldap.GetNameFromDN(split[split.Length - 1]);
        }

        public static string GetNewADsPath(this DirectoryEntry de, string dn)
        {
            var split = de.Path.Split(_splitChars);
            split[split.Length - 1] = dn;
            return string.Join("/", split);
        }

        public static SecurityIdentifier GetSid(this DirectoryEntry source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var objectSid = (byte[])source.Properties["objectSid"].Value;
            return (objectSid != null ? new SecurityIdentifier(objectSid, 0) : null);
        }
        public static SecurityIdentifier GetSid(this SearchResult source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var values = source.Properties["objectSid"];
            return (values != null && values.Count > 0 && values[0] is byte[] ? new SecurityIdentifier((byte[])values[0], 0) : null);
        }

        public static T GetSingleValue<T>(this PropertyCollection source, string propertyName)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var values = source[propertyName];
            return (values != null && values.Count > 0 && values[0] is T ? (T)values[0] : default(T));
        }
        public static T GetSingleValue<T>(this ResultPropertyCollection source, string propertyName)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var values = source[propertyName];
            return (values != null && values.Count > 0 && values[0] is T ? (T)values[0] : default(T));
        }

        public static IEnumerable<T> GetValues<T>(this PropertyCollection source, string propertyName)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var values = source[propertyName];
            return (values != null && values.Count > 0 ? values.Cast<T>() : new T[] { });
        }
        public static IEnumerable<T> GetValues<T>(this ResultPropertyCollection source, string propertyName)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var values = source[propertyName];
            return (values != null && values.Count > 0 ? values.Cast<T>() : new T[] { });
        }

        public static IEnumerable<T> QueryByIdentities<T>(this DirectoryEntry source, string identityProperty, string[] propertiesToLoad, IEnumerable<string> identities, Func<SearchResult, T> selector) { return source.QueryByIdentities<string, T>(identityProperty, propertiesToLoad, identities, selector, null); }
        public static IEnumerable<T> QueryByIdentities<TSource, T>(this DirectoryEntry source, string identityProperty, string[] propertiesToLoad, IEnumerable<TSource> identities, Func<SearchResult, T> selector, QueryInfo queryInfo)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (string.IsNullOrEmpty(identityProperty))
                throw new ArgumentNullException("identityProperty");
            if (propertiesToLoad == null || propertiesToLoad.Length == 0)
                propertiesToLoad = new string[] { };
            if (selector == null)
                throw new ArgumentNullException("selector");
            if (queryInfo == null)
                queryInfo = new QueryInfo();
            //
            var list = new List<T>();
            foreach (var batch in identities.GroupAt(queryInfo.BatchSize))
            {
                var filterPart = "(" + Ldap.EncodeFilter(identityProperty, true) + "=" + string.Join(")(" + Ldap.EncodeFilter(identityProperty, true) + "=", batch.Select(Ldap.EncodeFilter).ToArray()) + ")";
                var directorySearcher = new DirectorySearcher(source, string.Format(queryInfo.QueryFilter, "(|" + filterPart + ")"), propertiesToLoad);
                directorySearcher.PageSize = 1000;
                if (queryInfo.Limiter != null)
                    queryInfo.Limiter(directorySearcher);
                list.AddRange(
                    directorySearcher.FindAll()
                    .Cast<SearchResult>()
                    .Select(selector)
                    .OfType<T>());
            }
            return list;
        }

        public static IEnumerable<string> QueryDNsByInvoke(this DirectoryEntry source, string methodName, params object[] args)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var items = (IEnumerable)source.Invoke(methodName, args);
            return (items == null ? null : items.Cast<object>()
                .Select(item =>
                {
                    var itemAsAds = (item as UnsafeNativeMethods.IAds);
                    if (itemAsAds != null)
                        return Ldap.GetDNFromADsPath(itemAsAds.ADsPath);
                    var itemAsString = (item as string);
                    if (itemAsString != null)
                        return itemAsString;
                    return null;
                })
                .Where(x => x != null)
                .ToList());
        }

        public static IEnumerable<string> QueryDNsByInvokeGet(this DirectoryEntry source, string propertyName)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var members = (IEnumerable)source.InvokeGet(propertyName);
            return (members == null ? null : members.Cast<object>()
                .Select(item =>
                {
                    var itemAsString = (item as string);
                    if (itemAsString != null)
                        return itemAsString;
                    var itemAsAds = (item as UnsafeNativeMethods.IAds);
                    if (itemAsAds != null)
                        return Ldap.GetDNFromADsPath(itemAsAds.ADsPath);
                    return null;
                })
                .Where(x => x != null)
                .ToList());
        }

        public static IEnumerable<string> QueryDirectReports(this Func<string, DirectoryEntry> source, string dn, HashSet<string> set)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (string.IsNullOrEmpty(dn))
                throw new ArgumentNullException("dn");
            if (set == null)
                throw new ArgumentNullException("set");
            if (set.Contains(dn))
                throw new InvalidOperationException("Recursive directReports : " + dn);
            set.Add(dn);
            using (var de = source(dn))
            {
                var childDNs = de.QueryDNsByInvokeGet("directReports");
                if (childDNs == null)
                    yield break;
                foreach (var childDN in childDNs)
                {
                    yield return childDN;
                    var childs = source.QueryDirectReports(childDN, set);
                    if (childs == null)
                        yield break;
                    foreach (var child in childs)
                        yield return child;
                }
            }
        }

        public static string CreateUser(this Func<string, DirectoryEntry> source, bool whatIf, string objectName, string name)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (string.IsNullOrEmpty(objectName))
                throw new ArgumentNullException("objectName");
            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");
            Trace.TraceInformation("CreateUser: " + name);
            if (whatIf)
                return "C: " + name + "\n";
            using (var de = source(null))
            using (var newDe = de.Children.Add("CN=" + name, objectName))
            {
                newDe.Properties["userPrincipalName"].Value = name;
                newDe.CommitChanges();
            }
            return null;
        }

        public static string CreateUserProxy(this Func<string, DirectoryEntry> source, bool whatIf, string objectName, string name, SecurityIdentifier objectSid)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (string.IsNullOrEmpty(objectName))
                throw new ArgumentNullException("objectName");
            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");
            if (objectSid == null)
                throw new ArgumentNullException("objectSid");
            var objectSidAsBytes = new byte[objectSid.BinaryLength];
            objectSid.GetBinaryForm(objectSidAsBytes, 0);
            var sid = "{\\" + BitConverter.ToString(objectSidAsBytes).Replace("-", "\\") + "}";
            Trace.TraceInformation("CreateUserProxy: " + name + " " + sid);
            if (whatIf)
                return "CP: " + name + " " + sid + "\n";
            using (var de = source(null))
            using (var newDe = de.Children.Add("CN=" + name, objectName))
            {
                newDe.Properties["objectSid"].Clear();
                newDe.Properties["objectSid"].Value = objectSidAsBytes;
                newDe.Properties["userPrincipalName"].Value = name;
                newDe.CommitChanges();
            }
            return null;
        }

        public static string MovePrincipal(this Func<string, DirectoryEntry> source, bool whatIf, string sourceDN, string targetDN)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (string.IsNullOrEmpty(sourceDN))
                throw new ArgumentNullException("sourceDN");
            if (string.IsNullOrEmpty(targetDN))
                throw new ArgumentNullException("targetDN");
            Trace.TraceInformation("MovePrincipal: " + sourceDN + "->" + targetDN);
            if (whatIf)
                return "R: " + sourceDN + "->" + targetDN + "\n";
            using (var sourceDE = source(sourceDN))
            using (var targetDE = source(Ldap.GetContainerFromDN(targetDN)))
                sourceDE.MoveTo(targetDE, "CN=" + Ldap.GetNameFromDN(targetDN));
            return null;
        }

        public static string PurgePrincipal(this Func<string, DirectoryEntry> source, bool whatIf, string dn)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (string.IsNullOrEmpty(dn))
                throw new ArgumentNullException("dn");
            Trace.TraceInformation("Delete: " + dn);
            if (whatIf)
                return "P: " + dn + "\n";
            using (var de = source(dn))
                de.DeleteTree();
            return null;
        }

        public static string MergeGroupMembership(this Func<string, DirectoryEntry> source, bool whatIf, string dn, IEnumerable<string> members, bool removeMembers, out int inserts, out int deletes)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (string.IsNullOrEmpty(dn))
                throw new ArgumentNullException("dn");
            if (members == null)
                throw new ArgumentNullException("members");
            Trace.TraceInformation("Merge: " + dn);
            var b = new StringBuilder();
            //
            deletes = 0;
            inserts = 0;
            using (var rootDE = source("/"))
            {
                var de = source(dn);
                try
                {
                    var deMembers = de.QueryDNsByInvoke("members", null);
                    var deRemoves = deMembers.Except(members, StringComparer.OrdinalIgnoreCase).ToList();
                    deletes = deRemoves.Count;
                    if (deletes > 0)
                        foreach (var g in deRemoves.GroupAt(100))
                        {
                            if (whatIf)
                                foreach (var v in g)
                                    b.AppendLine("MD: " + v);
                            else
                                b.Append(source.MergeGroupMembership_Delete(ref de, dn, g));
                        }
                    //
                    var deInserts = members.Except(deMembers, StringComparer.OrdinalIgnoreCase).ToList();
                    inserts = deInserts.Count;
                    if (inserts > 0)
                        foreach (var g in deInserts.GroupAt(50))
                        {
                            if (whatIf)
                                foreach (var v in g)
                                    b.AppendLine("MI: " + v);
                            else
                                b.Append(source.MergeGroupMembership_Insert(rootDE, ref de, dn, g));
                        }
                }
                catch { if (de != null) de.Dispose(); }
                return (b.Length > 0 ? b.ToString() : null);
            }
        }

        private static string MergeGroupMembership_Delete(this Func<string, DirectoryEntry> source, ref DirectoryEntry de, string dn, IEnumerable<string> g)
        {
            try
            {
                foreach (var v in g)
                {
                    Trace.TraceInformation("MD: " + v);
                    de.Properties["member"].Remove(v);
                }
                de.CommitChanges();
            }
            catch
            {
                { if (de != null) de.Dispose(); de = source(dn); }
                // regroup by 10
                var b = new StringBuilder();
                foreach (var g2 in g.GroupAt(10))
                    try
                    {
                        foreach (var v in g2)
                            de.Properties["member"].Remove(v);
                        de.CommitChanges();
                    }
                    catch
                    {
                        { if (de != null) de.Dispose(); de = source(dn); }
                        // regroup by 1
                        foreach (var v in g2)
                            try { de.Properties["member"].Remove(v); de.CommitChanges(); }
                            catch
                            {
                                { if (de != null) de.Dispose(); de = source(dn); }
                                Trace.TraceInformation("xMD: " + v); b.AppendLine("xMD: " + v);
                            }
                    }
                return b.ToString();
            }
            return null;
        }

        private static string MergeGroupMembership_Insert(this Func<string, DirectoryEntry> source, DirectoryEntry rootDE, ref DirectoryEntry de, string dn, IEnumerable<string> g)
        {
            try
            {
                foreach (var v in g)
                {
                    Trace.TraceInformation("MI: " + v); Trace.Flush();
                    de.Properties["member"].Add(v);
                }
                de.CommitChanges();
            }
            catch
            {
                { if (de != null) de.Dispose(); de = source(dn); }
                // lookup items
                g = rootDE.QueryByIdentities<string>("distinguishedName", null, g, x => x.GetDN());
                try
                {
                    foreach (var v in g)
                    {
                        Trace.TraceInformation("MI: " + v);
                        de.Properties["member"].Add(v);
                    }
                    de.CommitChanges();
                }
                catch
                {
                    { if (de != null) de.Dispose(); de = source(dn); }
                    // regroup by 10
                    var b = new StringBuilder();
                    foreach (var g2 in g.GroupAt(10))
                        try
                        {
                            foreach (var v in g2)
                                de.Properties["member"].Add(v);
                            de.CommitChanges();
                        }
                        catch
                        {
                            if (de != null) de.Dispose(); de = source(dn);
                            // regroup by 1
                            foreach (var v in g2)
                                try { de.Properties["member"].Add(v); de.CommitChanges(); }
                                catch
                                {
                                    { if (de != null) de.Dispose(); de = source(dn); }
                                    Trace.TraceInformation("xMI: " + v); b.AppendLine("xMI: " + v);
                                }
                        }
                    return b.ToString();
                }
            }
            return null;
        }
    }
}
